/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once

#include <baja/common.hpp>
#include <baja/graphics.hpp>

namespace baja { namespace graphics { namespace scenes { namespace details {

namespace m = baja::math;
namespace g = baja::graphics;

// The scene_node_base template class is a class which implements the iscene_node methods
// and should be used when implementing the actual class for the scene node interfaces
// that derive from iscene_node
template <class IInterface> 
class scene_node_base : public IInterface, public std::enable_shared_from_this<IInterface>
{
public:
    // constructor
    scene_node_base() : 
        _priority(0), 
        _scale(1.f, 1.f, 1.f), 
        _standardEffect(standard_effect::meshLambert),
        _nodeDirty(true),
        _isVisible(true)
    {
    }

    // destructor
    virtual ~scene_node_base()
    {
        this->removeAllChildren(); // TEST: this used to be done in FinalRelease
    }

    // fast accessors
    bool getIsVisible() const
    {
        return _isVisible; 
    }

    void setNodeDirty() 
    {
        _nodeDirty = true; 
    }

    const m::matrix44& getLocalToWorld() const
    {
        return _localToWorld;
    }

    const m::matrix44& getWorldToLocal() const
    {
        return _worldToLocal;
    }

    const m::matrix44& getRotation() const
    {
        return _rotation;
    }

    const m::vector3d& getScale() const
    { 
        return _scale; 
    }

    const m::point3d& getLocalPosition() const
    { 
        return _position; 
    }

    std::shared_ptr<ieffect2> getEffect() const
    { 
        return _effect;
    }

    standard_effect getDefaultEffect() const
    {
        return _standardEffect; 
    }

    // default render, which does nothing
    virtual void render(const std::shared_ptr<iengine>& graphics)
    {
        this->renderInternal(graphics, &_localToWorld);
        return;

        /*
        // NYI: renderhook
        if (_renderHook == nullptr)
        {
            // no render hook set, just do the render and return
            hr = this->RenderInternal(graphics, &_localToWorld);
            return hr;
        }

        // call the pre-render pass for the render hook
        hr = _renderHook->Invoke(this, graphics, &_localToWorld, true);

        if (SUCCEEDED(hr))
        {
            // render
            hr = this->RenderInternal(graphics, &_localToWorld);
        }

        if (SUCCEEDED(hr))
        {
            // call post-render pass
            hr = _renderHook->Invoke(this, graphics, &_localToWorld, false);
        }

        return S_OK;
        */
    }

    // sets the default effect to use when rendering the scene node
    void setDefaultEffect(const standard_effect defaultEffect) 
    {
        _standardEffect = defaultEffect;
    }

    // gets the custom effect associated with the scene node
    std::shared_ptr<ieffect2> getCustomEffect()
    {
        return _effect;
    }

    // sets the custom effect associated with the scene node
    void setCustomEffect(const std::shared_ptr<ieffect2>& effect)
    {
        _effect = effect;
    }

    // updates transforms for this node and all nodes under it
    void updateTransforms(
        const m::matrix44& parentToWorld, 
        const m::matrix44& worldToParent, 
        bool forceUpdate
        )
    {
        // force update if this node is dirty
        if (_nodeDirty)
        {
            forceUpdate = true;
        }

        // clear the dirty flag
        _nodeDirty = false;

        // compute this node's transforms
        if (forceUpdate)
        {
            m::matrix44 childToParent;
            m::matrix44 parentToChild;

            this->getLocalToParent(&childToParent);
            childToParent.getInverse(&parentToChild);

            // compute "child to world" and "world to child" transforms
            parentToWorld.apply(childToParent, &_localToWorld);
            parentToChild.apply(worldToParent, &_worldToLocal);
        }

        // call on children of this node
        size_t childCount = 0;
        size_t i;
        childCount = _children.size();
        for (i = 0; i < childCount; i++)
        {
            std::shared_ptr<iscene_node> child;
            child = this->getChild(i);
            child->updateTransforms(_localToWorld, _worldToLocal, forceUpdate);
        }
    }

    // gets the "local to parent space" transform
    void getLocalToParent(m::matrix44* localToParent)
    {
        BAJA_VERIFY(localToParent);

        m::matrix44 rotAndTranslate(_rotation);
        rotAndTranslate.setTranslation(_position);

        m::matrix44 scale(_scale.x, _scale.y, _scale.z);

        rotAndTranslate.apply(scale, localToParent);
    }

    // gets the "local to world space" transform
    void getLocalToWorld(m::matrix44* localToWorld)
    {
        BAJA_VERIFY(localToWorld);

        *localToWorld = _localToWorld;
    }

    // gets the "world to local space" transform
    void getWorldToLocal(m::matrix44* worldToLocal)
    {
        BAJA_VERIFY(worldToLocal);

        *worldToLocal = _worldToLocal;
    }

    // gets the local position
    void getLocalPosition(m::point3d* position)
    {
        BAJA_VERIFY(position);

        *position = _position;
    }

    // gets the world position
    void getWorldPosition(m::point3d* position)
    {
        BAJA_VERIFY(position);

        _localToWorld.getTranslation(position);
    }

    // sets the position
    void setPosition(const m::point3d& position)
    {
        _position = position;
        _nodeDirty = true;
    }

    // Gets the scale
    void getScale(m::vector3d* scale)
    {
        BAJA_VERIFY(scale);

        *scale = _scale;
    }

    // Sets the scale
    void setScale(const m::vector3d& scale)
    {
        _scale = scale;
        _nodeDirty = true;
    }

    // Sets uniform scale
    void setScaleUniform(const float32 scale)
    {
        _scale.x = scale;
        _scale.y = scale;
        _scale.z = scale;
        _nodeDirty = true;
    }

    // Gets the rotation
    void getRotation(m::matrix44* rotation)
    {
        BAJA_VERIFY(rotation);

        *rotation = _rotation;
    }

    // Sets the rotation
    void setRotation(const m::matrix44& rotation)
    {
        _rotation = rotation;
        _nodeDirty = true;
    }

    // Get children count
    size_t getChildCount()
    {
        return _children.size();
    }

    // Gets child at the given index
    std::shared_ptr<iscene_node> getChild(size_t childIndex)
    {
        BAJA_VERIFY(childIndex < _children.size());

        return _children[childIndex];

    }

    bool containsChild(const std::shared_ptr<iscene_node>& child)
    {
        size_t totalChildren = _children.size();
        size_t i;
        for (i = 0; i < totalChildren; i++)
        {
            if (_children[i] == child)
            {
                return true;
            }
        }

        return false;
    }

    // adds a child to this scene node
    void addChild(const std::shared_ptr<iscene_node>& child)
    {
        BAJA_VERIFY(child);

        // make sure parent's child is nullptr
        std::shared_ptr<iscene_node> parent;
        parent = child->getParent();
        if (parent.get() != nullptr)
        {
            BAJA_THROW(std::logic_error("")); // ERROR:
        }

        _children.push_back(child);
        
        child->setParent(shared_from_this());
    }

    // removes a child from this scene node
    void removeChild(const std::shared_ptr<iscene_node>& child)
    {
        // find child in our array of children
        size_t totalChildren = _children.size();
        size_t i;
        for (i = 0; i < totalChildren; i++)
        {
            if (_children[i] == child)
            {
                break;
            }
        }

        // if found, continue to remove the child
        if (i < totalChildren)
        {
            this->removeChildAtIndex(i);
        }
    }

    // removes child at the specified index from this scene node
    void removeChildAtIndex(const size_t childIndex)
    {
        BAJA_VERIFY(childIndex < _children.size());

        std::shared_ptr<iscene_node> child = _children[childIndex];

        if (child.get() != nullptr)
        {
            // clear the child's parent
            child->setParent(nullptr);
        }

        // remove child from the array
        _children.erase(_children.begin() + childIndex);
    }

    // removes all children from the scene node
    void removeAllChildren()
    {
        // remove all children
        size_t totalChildren = _children.size();
        size_t i;
        for (i = 0; i < totalChildren; i++)
        {
            std::shared_ptr<iscene_node> child = _children[i];
            if (child != nullptr)
            {
                child->setParent(nullptr);
            }
        }

        _children.clear();
    }

    // gets the parent of this scene node
    std::shared_ptr<iscene_node> getParent()
    {
        return _parent.lock();
    }

    // sets the parent of this scene node
    void setParent(const std::shared_ptr<iscene_node>& parent)
    {
        _parent = parent;
        _nodeDirty = true;
    }

    // gets if scene node is visible or not
    bool getIsVisible()
    {
        return _isVisible;
    }

    // sets visibility state of the scene node
     void setIsVisible(bool isVisible)
    {
        _isVisible = isVisible; 
    }

     /*
    // NYI:
    // gets the input events of this scene node
    GetInputEvents(IUnknown** inputEvents)
    {
        return _inputEvents.CopyTo(inputEvents);
    }

    //
    // sets the input events of this scene node
    //
    virtual HRESULT __stdcall 
    SetInputEvents(IUnknown* inputEvents)
    {
        _inputEvents = inputEvents;

        return S_OK;
    }
    */

    // gets this scene node's relative render priority
    uint32 getRenderPriority()
    {
        return _priority;
    }

    // sets this scene node's relative render priority
    void setRenderPriority(const int32 pri)
    {
        _priority = pri;
    }

    /*
    // NYI:
    //
    // sets a render hook for the scene node
    //
    virtual HRESULT __stdcall 
    SetRenderHook(IGxRenderHookDelegate* hook)
    {
        _renderHook = hook;

        return S_OK;
    }

    //
    // allows the scene graph node to specify custom wait handles
    // for timers, etc.
    //
    virtual HRESULT __stdcall 
    AddWaitHandles(
        __out_ecount(handleCount) HANDLE* handles, 
        __in uint32 handleCount,
        __out uint32* handlesUsed
        )
    {
        TRACEHR hr = S_OK;
        uint32 totalHandlesUsed = 0;
        
        for (uint32 i = 0; SUCCEEDED(hr) && (i < _children.size()); i++)
        {
            // Allow children to add their wait handles
            uint32 myHandlesUsed = 0;
            hr = _children[i]->AddWaitHandles(
                handles,
                handleCount,
                &myHandlesUsed
                );

            handles += myHandlesUsed;
            handleCount -= myHandlesUsed;
            totalHandlesUsed += myHandlesUsed;
        }
        
        *handlesUsed = totalHandlesUsed;
        return hr;
    }

    //
    // called when a wait handle fires.
    //
    virtual HRESULT __stdcall 
    OnWaitHandle(
        __in HANDLE waitHandle,
        __out_opt bool* handled
        )
    {
        TRACEHR hr = S_OK;
        bool myHandled = false;
        
        for (uint32 i = 0; SUCCEEDED(hr) && (!myHandled) && (i < _children.size()); i++)
        {
            // Allow children to react to the wait handle
            hr = _children[i]->OnWaitHandle(
                waitHandle,
                &myHandled
                );
        }

        if (handled != nullptr)
        {
            *handled = myHandled;
        }
        
        return hr;
    }
*/

    // performs hit testing of specified pixel to the scene node.
    virtual bool pixelHitTest(
        const std::shared_ptr<iscene_transformer>& sceneTransformer,
        const m::point2d& pixel,
        m::point3d* hitPositionLocalSpace)
    {
        return false;
    }

protected:
    // derived classes must implement renderInternal()
    virtual void renderInternal(const std::shared_ptr<iengine>& graphics, const m::matrix44* localToWorld) = 0;

private:

    m::matrix44 _localToWorld;
    m::matrix44 _worldToLocal;
    m::matrix44 _rotation;

    m::vector3d _scale;
    m::point3d _position;

    std::vector<std::shared_ptr<iscene_node>> _children;
    std::shared_ptr<ieffect2> _effect;
    // NYI: std::shared_ptr<IGxRenderHookDelegate> _renderHook;
    // NYI: Auto<IUnknown> _inputEvents;

    std::weak_ptr<iscene_node> _parent;

    standard_effect _standardEffect;

    int32 _priority;

    bool _nodeDirty;
    bool _isVisible;
};

}}}} // namespace baja::graphics::scenes::details
